﻿//--------------------- Copyright Block ----------------------
/* 

PrayTimes.js: Prayer Times Calculator (ver 2.3)
Copyright (C) 2007-2011 PrayTimes.org

Developer: Hamid Zarrabi-Zadeh
License: GNU LGPL v3.0

TERMS OF USE:
Permission is granted to use this code, with or 
without modification, in any website or application 
provided that credit is given to the original work 
with a link back to PrayTimes.org.

This program is distributed in the hope that it will 
be useful, but WITHOUT ANY WARRANTY. 

PLEASE DO NOT REMOVE THIS COPYRIGHT BLOCK.
 
*/


//--------------------- Help and Manual ----------------------
/*

User's Manual: 
http://praytimes.org/manual

Calculation Formulas: 
http://praytimes.org/calculation



//------------------------ User Interface -------------------------


getTimes (date, coordinates [, timeZone [, dst [, timeFormat]]]) 
	
setMethod (method)       // set calculation method 
adjust (parameters)      // adjust calculation parameters	
tune (offsets)           // tune times by given offsets 

getMethod ()             // get calculation method 
getSetting ()            // get current calculation parameters
getOffsets ()            // get current time offsets


//------------------------- Sample Usage --------------------------


var PT = new PrayTimes('ISNA');
var times = PT.getTimes(new Date(), [43, -80], -5);
document.write('Sunrise = '+ times.sunrise)


*/


//----------------------- PrayTimes Class ------------------------


function PrayTimes(method) {


    //------------------------ Constants --------------------------
    var 

    // Time Names
	timeNames = {
	    imsak: 'Imsak',
	    fajr: 'Fajr',
	    sunrise: 'Sunrise',
	    dhuhr: 'Dhuhr',
	    asr: 'Asr',
	    sunset: 'Sunset',
	    maghrib: 'Maghrib',
	    isha: 'Isha',
	    midnight: 'Midnight'
	},


    // Calculation Methods
	methods = {
	    MWL: {
	        name: 'Muslim World League',
	        params: { fajr: 18, isha: 17}
	    },
	    ISNA: {
	        name: 'Islamic Society of North America (ISNA)',
	        params: { fajr: 15, isha: 15}
	    },
	    Egypt: {
	        name: 'Egyptian General Authority of Survey',
	        params: { fajr: 19.5, isha: 17.5}
	    },
	    Makkah: {
	        name: 'Umm Al-Qura University, Makkah',
	        params: { fajr: 18.5, isha: '90 min'}
	    },  // fajr was 19 degrees before 1430 hijri
	    Karachi: {
	        name: 'University of Islamic Sciences, Karachi',
	        params: { fajr: 18, isha: 18}
	    },
	    Tehran: {
	        name: 'Institute of Geophysics, University of Tehran',
	        params: { fajr: 17.7, isha: 14, maghrib: 4.5, midnight: 'Jafari'}
	    },  // isha is not explicitly specified in this method
	    Jafari: {
	        name: 'Shia Ithna-Ashari, Leva Institute, Qum',
	        params: { fajr: 16, isha: 14, maghrib: 4, midnight: 'Jafari'}
	    }
	},


    // Default Parameters in Calculation Methods
	defaultParams = {
	    maghrib: '0 min', midnight: 'Standard'

	},


    //----------------------- Parameter Values ----------------------
    /*
	
    // Asr Juristic Methods
    asrJuristics = [ 
    'Standard',    // Shafi`i, Maliki, Ja`fari, Hanbali
    'Hanafi'       // Hanafi
    ],


    // Midnight Mode
    midnightMethods = [ 
    'Standard',    // Mid Sunset to Sunrise
    'Jafari'       // Mid Sunset to Fajr
    ],


    // Adjust Methods for Higher Latitudes
    highLatMethods = [
    'NightMiddle', // middle of night
    'AngleBased',  // angle/60th of night
    'OneSeventh',  // 1/7th of night
    'None'         // No adjustment
    ],


    // Time Formats
    timeFormats = [
    '24h',         // 24-hour format
    '12h',         // 12-hour format
    '12hNS',       // 12-hour format with no suffix
    'Float'        // floating point number 
    ],
    */


    //---------------------- Default Settings --------------------

	calcMethod = 'MWL',

    // do not change anything here; use adjust method instead
	setting = {
	    imsak: '10 min',
	    dhuhr: '0 min',
	    asr: 'Standard',
	    highLats: 'NightMiddle'
	},

	timeFormat = '24h',
	timeSuffixes = ['am', 'pm'],
	invalidTime = '-----',

	numIterations = 1,
	offset = {},


    //----------------------- Local Variables ---------------------

	lat, lng, elv,       // coordinates
	timeZone, jDate;     // time variables


    //---------------------- Initialization -----------------------


    // set methods defaults
    var defParams = defaultParams;
    for (var i in methods) {
        var params = methods[i].params;
        for (var j in defParams)
            if ((typeof (params[j]) == 'undefined'))
                params[j] = defParams[j];
    };

    // initialize settings
    calcMethod = methods[method] ? method : calcMethod;
    var params = methods[calcMethod].params;
    for (var id in params)
        setting[id] = params[id];

    // init time offsets
    for (var i in timeNames)
        offset[i] = 0;



    //----------------------- Public Functions ------------------------
    return {


        // set calculation method 
        setMethod: function (method) {
            if (methods[method]) {
                this.adjust(methods[method].params);
                calcMethod = method;
            }
        },


        // set calculating parameters
        adjust: function (params) {
            for (var id in params)
                setting[id] = params[id];
        },


        // set time offsets
        tune: function (timeOffsets) {
            for (var i in timeOffsets)
                offset[i] = timeOffsets[i];
        },


        // get current calculation method
        getMethod: function () { return calcMethod; },

        // get current setting
        getSetting: function () { return setting; },

        // get current time offsets
        getOffsets: function () { return offset; },

        // get default calc parametrs
        getDefaults: function () { return methods; },


        // return prayer times for a given date
        getTimes: function (date, coords, timezone, dst, format) {
            lat = 1 * coords[0];
            lng = 1 * coords[1];
            elv = coords[2] ? 1 * coords[2] : 0;
            timeFormat = format || timeFormat;
            if (date.constructor === Date)
                date = [date.getFullYear(), date.getMonth() + 1, date.getDate()];
            if (typeof (timezone) == 'undefined' || timezone == 'auto')
                timezone = this.getTimeZone(date);
            if (typeof (dst) == 'undefined' || dst == 'auto')
                dst = this.getDst(date);
            timeZone = 1 * timezone + (1 * dst ? 1 : 0);
            jDate = this.julian(date[0], date[1], date[2]) - lng / (15 * 24);

            return this.computeTimes();
        },


        // convert float time to the given format (see timeFormats)
        getFormattedTime: function (time, format, suffixes) {
            if (isNaN(time))
                return invalidTime;
            if (format == 'Float') return time;
            suffixes = suffixes || timeSuffixes;

            time = DMath.fixHour(time + 0.5 / 60);  // add 0.5 minutes to round
            var hours = Math.floor(time);
            var minutes = Math.floor((time - hours) * 60);
            var suffix = (format == '12h') ? suffixes[hours < 12 ? 0 : 1] : '';
            var hour = (format == '24h') ? this.twoDigitsFormat(hours) : ((hours + 12 - 1) % 12 + 1);
            return hour + ':' + this.twoDigitsFormat(minutes) + (suffix ? ' ' + suffix : '');
        },


        //---------------------- Calculation Functions -----------------------


        // compute mid-day time
        midDay: function (time) {
            var eqt = this.sunPosition(jDate + time).equation;
            var noon = DMath.fixHour(12 - eqt);
            return noon;
        },


        // compute the time at which sun reaches a specific angle below horizon
        sunAngleTime: function (angle, time, direction) {
            var decl = this.sunPosition(jDate + time).declination;
            var noon = this.midDay(time);
            var t = 1 / 15 * DMath.arccos((-DMath.sin(angle) - DMath.sin(decl) * DMath.sin(lat)) /
				(DMath.cos(decl) * DMath.cos(lat)));
            return noon + (direction == 'ccw' ? -t : t);
        },


        // compute asr time 
        asrTime: function (factor, time) {
            var decl = this.sunPosition(jDate + time).declination;
            var angle = -DMath.arccot(factor + DMath.tan(Math.abs(lat - decl)));
            return this.sunAngleTime(angle, time);
        },


        // compute declination angle of sun and equation of time
        // Ref: http://aa.usno.navy.mil/faq/docs/SunApprox.php
        sunPosition: function (jd) {
            var D = jd - 2451545.0;
            var g = DMath.fixAngle(357.529 + 0.98560028 * D);
            var q = DMath.fixAngle(280.459 + 0.98564736 * D);
            var L = DMath.fixAngle(q + 1.915 * DMath.sin(g) + 0.020 * DMath.sin(2 * g));

            var R = 1.00014 - 0.01671 * DMath.cos(g) - 0.00014 * DMath.cos(2 * g);
            var e = 23.439 - 0.00000036 * D;

            var RA = DMath.arctan2(DMath.cos(e) * DMath.sin(L), DMath.cos(L)) / 15;
            var eqt = q / 15 - DMath.fixHour(RA);
            var decl = DMath.arcsin(DMath.sin(e) * DMath.sin(L));

            return { declination: decl, equation: eqt };
        },


        // convert Gregorian date to Julian day
        // Ref: Astronomical Algorithms by Jean Meeus
        julian: function (year, month, day) {
            if (month <= 2) {
                year -= 1;
                month += 12;
            };
            var A = Math.floor(year / 100);
            var B = 2 - A + Math.floor(A / 4);

            var JD = Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5;
            return JD;
        },


        //---------------------- Compute Prayer Times -----------------------


        // compute prayer times at given julian date
        computePrayerTimes: function (times) {
            times = this.dayPortion(times);
            var params = setting;

            var imsak = this.sunAngleTime(this.eval(params.imsak), times.imsak, 'ccw');
            var fajr = this.sunAngleTime(this.eval(params.fajr), times.fajr, 'ccw');
            var sunrise = this.sunAngleTime(this.riseSetAngle(), times.sunrise, 'ccw');
            var dhuhr = this.midDay(times.dhuhr);
            var asr = this.asrTime(this.asrFactor(params.asr), times.asr);
            var sunset = this.sunAngleTime(this.riseSetAngle(), times.sunset); ;
            var maghrib = this.sunAngleTime(this.eval(params.maghrib), times.maghrib);
            var isha = this.sunAngleTime(this.eval(params.isha), times.isha);

            return {
                imsak: imsak, fajr: fajr, sunrise: sunrise, dhuhr: dhuhr,
                asr: asr, sunset: sunset, maghrib: maghrib, isha: isha
            };
        },


        // compute prayer times 
        computeTimes: function () {
            // default times
            var times = {
                imsak: 5, fajr: 5, sunrise: 6, dhuhr: 12,
                asr: 13, sunset: 18, maghrib: 18, isha: 18
            };

            // main iterations
            for (var i = 1; i <= numIterations; i++)
                times = this.computePrayerTimes(times);

            times = this.adjustTimes(times);

            // add midnight time
            times.midnight = (setting.midnight == 'Jafari') ?
				times.sunset + this.timeDiff(times.sunset, times.fajr) / 2 :
				times.sunset + this.timeDiff(times.sunset, times.sunrise) / 2;

            times = this.tuneTimes(times);
            return this.modifyFormats(times);
        },


        // adjust times 
        adjustTimes: function (times) {
            var params = setting;
            for (var i in times)
                times[i] += timeZone - lng / 15;

            if (params.highLats != 'None')
                times = this.adjustHighLats(times);

            if (this.isMin(params.imsak))
                times.imsak = times.fajr - this.eval(params.imsak) / 60;
            if (this.isMin(params.maghrib))
                times.maghrib = times.sunset + this.eval(params.maghrib) / 60;
            if (this.isMin(params.isha))
                times.isha = times.maghrib + this.eval(params.isha) / 60;
            times.dhuhr += this.eval(params.dhuhr) / 60;

            return times;
        },


        // get asr shadow factor
        asrFactor: function (asrParam) {
            var factor = { Standard: 1, Hanafi: 2}[asrParam];
            return factor || this.eval(asrParam);
        },


        // return sun angle for sunset/sunrise
        riseSetAngle: function () {
            //var earthRad = 6371009; // in meters
            //var angle = DMath.arccos(earthRad/(earthRad+ elv));
            var angle = 0.0347 * Math.sqrt(elv); // an approximation
            return 0.833 + angle;
        },


        // apply offsets to the times
        tuneTimes: function (times) {
            for (var i in times)
                times[i] += offset[i] / 60;
            return times;
        },


        // convert times to given time format
        modifyFormats: function (times) {
            for (var i in times)
                times[i] = this.getFormattedTime(times[i], timeFormat);
            return times;
        },


        // adjust times for locations in higher latitudes
        adjustHighLats: function (times) {
            var params = setting;
            var nightTime = this.timeDiff(times.sunset, times.sunrise);

            times.imsak = this.adjustHLTime(times.imsak, times.sunrise, this.eval(params.imsak), nightTime, 'ccw');
            times.fajr = this.adjustHLTime(times.fajr, times.sunrise, this.eval(params.fajr), nightTime, 'ccw');
            times.isha = this.adjustHLTime(times.isha, times.sunset, this.eval(params.isha), nightTime);
            times.maghrib = this.adjustHLTime(times.maghrib, times.sunset, this.eval(params.maghrib), nightTime);

            return times;
        },


        // adjust a time for higher latitudes
        adjustHLTime: function (time, base, angle, night, direction) {
            var portion = this.nightPortion(angle, night);
            var timeDiff = (direction == 'ccw') ?
			this.timeDiff(time, base) :
			this.timeDiff(base, time);
            if (isNaN(time) || timeDiff > portion)
                time = base + (direction == 'ccw' ? -portion : portion);
            return time;
        },


        // the night portion used for adjusting times in higher latitudes
        nightPortion: function (angle, night) {
            var method = setting.highLats;
            var portion = 1 / 2 // MidNight
            if (method == 'AngleBased')
                portion = 1 / 60 * angle;
            if (method == 'OneSeventh')
                portion = 1 / 7;
            return portion * night;
        },


        // convert hours to day portions 
        dayPortion: function (times) {
            for (var i in times)
                times[i] /= 24;
            return times;
        },


        //---------------------- Time Zone Functions -----------------------


        // get local time zone
        getTimeZone: function (date) {
            var year = date[0];
            var t1 = this.gmtOffset([year, 0, 1]);
            var t2 = this.gmtOffset([year, 6, 1]);
            return Math.min(t1, t2);
        },


        // get daylight saving for a given date
        getDst: function (date) {
            return 1 * (this.gmtOffset(date) != this.getTimeZone(date));
        },


        // GMT offset for a given date
        gmtOffset: function (date) {
            var localDate = new Date(date[0], date[1] - 1, date[2], 12, 0, 0, 0);
            var GMTString = localDate.toGMTString();
            var GMTDate = new Date(GMTString.substring(0, GMTString.lastIndexOf(' ') - 1));
            var hoursDiff = (localDate - GMTDate) / (1000 * 60 * 60);
            return hoursDiff;
        },


        //---------------------- Misc Functions -----------------------

        // convert given string into a number
        eval: function (str) {
            return 1 * (str + '').split(/[^0-9.+-]/)[0];
        },


        // detect if input contains 'min'
        isMin: function (arg) {
            return (arg + '').indexOf('min') != -1;
        },


        // compute the difference between two times 
        timeDiff: function (time1, time2) {
            return DMath.fixHour(time2 - time1);
        },


        // add a leading 0 if necessary
        twoDigitsFormat: function (num) {
            return (num < 10) ? '0' + num : num;
        }

    }
}



//---------------------- Degree-Based Math Class -----------------------


var DMath = {

    dtr: function (d) { return (d * Math.PI) / 180.0; },
    rtd: function (r) { return (r * 180.0) / Math.PI; },

    sin: function (d) { return Math.sin(this.dtr(d)); },
    cos: function (d) { return Math.cos(this.dtr(d)); },
    tan: function (d) { return Math.tan(this.dtr(d)); },

    arcsin: function (d) { return this.rtd(Math.asin(d)); },
    arccos: function (d) { return this.rtd(Math.acos(d)); },
    arctan: function (d) { return this.rtd(Math.atan(d)); },

    arccot: function (x) { return this.rtd(Math.atan(1 / x)); },
    arctan2: function (y, x) { return this.rtd(Math.atan2(y, x)); },

    fixAngle: function (a) { return this.fix(a, 360); },
    fixHour: function (a) { return this.fix(a, 24); },

    fix: function (a, b) {
        a = a - b * (Math.floor(a / b));
        return (a < 0) ? a + b : a;
    }
}


//---------------------- Init Object -----------------------


var prayTimes = new PrayTimes();

